Slots
Video Summary
Let's suppose we're building a CaptionedImage
component. Here's what we start with:
function CaptionedImage({ alt, src, caption }) { return ( <figure> <img alt={alt} src={src} /> <div className="divider" /> <figcaption>{caption}</figcaption> </figure> );}
It uses the <figure>
tag to include an attached caption, often used for graphs or decorative images (think "fig. 1" from a textbook!).
Here's the problem, though: I want to be able to specify multiple image sources!
For example, it's a best practice to include at least 2 versions of each image: one for standard-density displays, and one for high-density displays, like Apple's “Retina Display”.
Also, it can be good for performance to use modern image formats like avif
. The AVIF format produces images that are 25-75% smaller than JPEGs of comparable quality.
Not all browsers support AVIF though (at the time of writing, Safari is in the process of adding support for it). And so, we need to keep using the jpegs as a fallback
So, let's suppose I want to choose between 4 different image files:
meerkat.jpg
meerkat.avif
meerkat@2x.jpg
meerkat@2x.avif
The best way to do this is with a <picture>
tag:
<picture> <source type="image/avif" srcSet={` https://sandpack-bundler.vercel.app/img/meerkat.avif 1x, https://sandpack-bundler.vercel.app/img/meerkat@2x.avif 2x `} /> <source type="image/jpeg" srcSet={` https://sandpack-bundler.vercel.app/img/meerkat.jpg 1x, https://sandpack-bundler.vercel.app/img/meerkat@2x.jpg 2x `} /> <img alt="A meerkat looking curiously at the camera" src="https://sandpack-bundler.vercel.app/img/meerkat.jpg" /></picture>
Oh dear. That's a lot of stuff! And it's going to vary depending on the image!
The critical problem with our CaptionedImage
component is that there are so many valid possibilities for images.
The image could look like this:
<img alt="A punk-rock cat illustration" src="https://sandpack-bundler.vercel.app/img/punk-cat.png"/>
Or like this:
<picture> <source type="image/webp" srcSet={` https://sandpack-bundler.vercel.app/img/spaceship.webp 1x, https://sandpack-bundler.vercel.app/img/spaceship@2x.webp 2x, https://sandpack-bundler.vercel.app/img/spaceship@3x.webp 3x `} /> <source type="image/png" srcSet={` https://sandpack-bundler.vercel.app/img/spaceship.png 1x, https://sandpack-bundler.vercel.app/img/spaceship@2x.png 2x, https://sandpack-bundler.vercel.app/img/spaceship@3x.png 3x `} /> <img alt="A meerkat looking curiously at the camera" src="https://sandpack-bundler.vercel.app/img/meerkat.jpg" /></picture>
Or like this:
<svg width="67" height="70" viewBox="0 0 67 70" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M33.5 0C38.0348 0 38.1612 5.97002 42.474 7.37181C46.7869 8.7736 50.3971 4.01804 54.0658 6.68441C57.7346 9.35077 54.3288 14.2549 56.9943 17.9248C59.6598 21.5948 65.3748 19.8701 66.7762 24.1844C68.1775 28.4987 62.5406 30.4637 0Z" fill="#0AB852" /> <path d="M21 34L30.36 43L47 27" stroke="white" stroke-width="8" stroke-linecap="round" stroke-linejoin="round" /></svg>
If we want our CaptionedImage
component to work with all images, we need to support a lot of variants!
We could try adding a bunch of props, and a whole bunch of logic to conditionally render different elements depending on those props... but there's a better solution: the slots pattern.
Here's what it looks like:
function CaptionedImage({ image, caption }) { return ( <figure> {image} <div className="divider" /> <figcaption>{caption}</figcaption> </figure> );}
// We'd use it like this:
<CaptionedImage image={ <img alt="A punk-rock cat illustration" src="https://sandpack-bundler.vercel.app/img/punk-cat.png" /> } caption="Illustration by Josh Comeau"/>
The image
prop takes a React element. Essentially, it creates a “slot” inside the <figure>
for the consumer to include whatever markup we need.
In a way, this isn't a new idea: we've been doing it since Module 1! But, up until now, it's always been through the children
prop.
// Using `children` instead of `image`:<CaptionedImage caption="Illustration by Josh Comeau"> <img alt="A punk-rock cat illustration" src="https://sandpack-bundler.vercel.app/img/punk-cat.png" /></CaptionedImage>
The children
prop isn't special. We can pass React elements to any prop, not just children
!
The beautiful thing about this pattern is that it allows us to include multiple slots in a single component. It's like being able to specify multiple distinct children!
For example: we can pass a React element for the caption
prop, if we want to include custom markup:
<CaptionedImage image={ <img alt="A meerkat looking curiously at the camera" src="https://sandpack-bundler.vercel.app/img/meerkat.jpg" /> } caption={ <> Photo by <a href="">Manuel Capellari</a>, shot in August 2019 and published on <strong>Unsplash</strong>. </> }/>
We've now specified two different React elements, to fill in two separate slots:
function CaptionedImage({ image, caption }) { return ( <figure> {image} {/* <— Slot 1 */} <div className="divider" /> <figcaption> {caption} {/* <— Slot 2 */} </figcaption> </figure> );}
This pattern provides maximum control and power to the consumer:
- With delegated props, we're able to provide additional props to a particular element
- With polymorphism, we're able to change a particular element's HTML tag
- With slots, we're able to provide any markup we want, without restrictions.
This isn't necessarily a good thing! It's a tradeoff. The more power we give the consumer, the more flexible our component is, but the more likely it is for things to go awry. The consumer can introduce bugs, use the component in ways never intended, and deviate from a consistent style.
But, in this case, there are just so many valid options for an image, I don't see any other practical way to solve the problem.
All of the patterns we've been learning about are useful in different situations. And we're building a toolbox full of useful tools!
Here's the starting code from the video above:
Code Playground
…And here's the final code, with all of our tweaks:
Code Playground